package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.DoublePair;
import it.unibo.refolding.common.DoubleQuartet;
import it.unibo.refolding.common.DoubleTriad;
import it.unibo.refolding.common.LoggingInvocationHandler;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.pico.csv.CsvDataExtractor;
import it.unibo.refolding.pico.jpk.JpkDataExtractor;
import it.unibo.refolding.pico.mfp.MfpDataExtractor;
import it.unibo.refolding.pico.stripchart.FetchRefoldBreakParams;
import it.unibo.refolding.pico.stripchart.JpHelper;
import it.unimi.dsi.fastutil.doubles.DoubleList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import it.unimi.dsi.fastutil.shorts.ShortList;
import it.unimi.dsi.fastutil.shorts.ShortLists;

import java.io.IOException;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.cli.Option;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jremoting.ConnectionException;
import org.codehaus.jremoting.RedirectedException;
import org.codehaus.jremoting.client.SocketDetails;
import org.codehaus.jremoting.client.monitors.ConsoleClientMonitor;
import org.codehaus.jremoting.client.pingers.NeverConnectionPinger;
import org.codehaus.jremoting.client.resolver.ServiceResolver;
import org.codehaus.jremoting.client.streams.ByteStream;
import org.codehaus.jremoting.client.transports.SocketTransport;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

public class PicoUtil {
  public static final String FILE_LIST_END = "\\*File list end";

  static final Pattern curveStretchPattern = Pattern.compile(String.format(
    "CurveStretch\\(r'(%s)', (%s), (%s), (%s)(?:, (%s))?\\)", "[^']*", CommonThings.FP_REGEX,
    CommonThings.FP_REGEX, "\\[[^]]*\\]", CommonThings.FP_REGEX));

  private static final Logger logger = Logger.getLogger(PicoUtil.class.getSimpleName());

  public static class Position {
    final int offset;
    final int length;

    public Position(int offset, int length) {
      this.offset = offset;
      this.length = length;
    }
  }

  public static short[] extractLittleEndianData(byte[] fileContent, Position p) {
    short[] dst = new short[p.length / 2];
    for (int i = 0; i < dst.length; ++i) {
      int j = p.offset + (i << 1);
      dst[i] = (short) ((fileContent[j + 1] << 8) | (fileContent[j] & 0xFF));
    }
    return dst;
  }

  public static String toNmPnString(List<DoublePair> a) {
    StringBuilder sb = new StringBuilder();
    for (DoublePair dp : a) {
      if (sb.length() != 0) {
        sb.append(", ");
      }
      sb.append(String.format(Locale.ROOT, "(%s, %s)", dp.x * 1e9, dp.y * 1e12));
    }
    return sb.toString();
  }

  public static String toNmPnNmString(List<DoubleTriad> a) {
    StringBuilder sb = new StringBuilder();
    for (DoubleTriad dt : a) {
      if (sb.length() != 0) {
        sb.append(", ");
      }
      sb.append(String.format(Locale.ROOT, "(%s, %s, %s)", dt.x * 1e9, dt.y * 1e12, dt.z * 1e9));
    }
    return sb.toString();
  }

  public static String toNmPnNmNmString(List<DoubleQuartet> a) {
    StringBuilder sb = new StringBuilder();
    for (DoubleQuartet dt : a) {
      if (sb.length() != 0) {
        sb.append(", ");
      }
      sb.append(String.format(
        Locale.ROOT, "(%s, %s, %s, %s)", dt.x * 1e9, dt.y * 1e12, dt.z * 1e9, dt.t * 1e9));
    }
    return sb.toString();
  }

  public static Option createDefLsbVsZLsbSlopeOption() {
    Option defLsbVsZLsbSlopeOption = new Option(
      "defLsbVsZLsbSlope", true, "defLsbVsZLsbSlope for files");
    defLsbVsZLsbSlopeOption.setRequired(true);
    defLsbVsZLsbSlopeOption.setType(String.class);
    return defLsbVsZLsbSlopeOption;
  }

  public static Option createSvgDefScaleOption() {
    Option svgDefScaleOption = new Option("svgDefScale", true, "svgDefScale for files");
    svgDefScaleOption.setRequired(true);
    svgDefScaleOption.setType(String.class);
    return svgDefScaleOption;
  }

  public static Option createSvgIterationOffsetPnOption() {
    Option svgIterationOffsetPnOption = new Option("svgIterationOffsetPn", true, "svgIterationOffsetPn for files");
    svgIterationOffsetPnOption.setRequired(true);
    svgIterationOffsetPnOption.setType(String.class);
    return svgIterationOffsetPnOption;
  }

  public static Option createXOffsetStdDevNmOption() {
    Option xOffsetStdDevNmOption = new Option(
      "xOffsetStdDevNm", true, "Standard deviation of movements on x axis.");
    xOffsetStdDevNmOption.setRequired(true);
    xOffsetStdDevNmOption.setType(Double.TYPE);
    return xOffsetStdDevNmOption;
  }

  public static Option createXOffsetChangeFrequencyOption() {
    Option xOffsetChangeFrequencyOption = new Option(
      "xOffsetChangeFrequency", true, "Standard deviation of movements on x axis.");
    xOffsetChangeFrequencyOption.setRequired(true);
    xOffsetChangeFrequencyOption.setType(Integer.TYPE);
    return xOffsetChangeFrequencyOption;
  }

  public static double generateXOffset(double oldXOffset, double xOffsetStdDevM, Random rnd) {
    final double HALF_RANGE = 10e-6;
    Preconditions.checkState(-HALF_RANGE < oldXOffset && oldXOffset < HALF_RANGE, oldXOffset);
    double xOffsetM = oldXOffset + rnd.nextGaussian() * xOffsetStdDevM;
    xOffsetM = Math.signum(xOffsetM) * (Math.abs(xOffsetM) % HALF_RANGE);
    return xOffsetM;
  }

  public static boolean isFakeOrRemoteNanoDriver(String nanoDriverClassname) {
    return NanoDriverUtil.isFakeNanoDriver(nanoDriverClassname)
      || nanoDriverClassname.equals("it.unibo.refolding.pico.RemoteNanoDriver")
      || nanoDriverClassname.equals("it.unibo.refolding.pico.LoggingInfoRemoteNanoDriver");
  }

  public static NanoDriver nanoDriver(
    String nanoDriverClassname, NanoDriverConfig config, String host, int port) {
    final NanoDriver nanoDriver;
    if (nanoDriverClassname.equals("it.unibo.refolding.pico.RemoteNanoDriver")) {
      nanoDriver = remoteNanoDriver(host, port);
    } else if (nanoDriverClassname.equals("it.unibo.refolding.pico.LoggingInfoRemoteNanoDriver")) {
      nanoDriver = (NanoDriver) Proxy.newProxyInstance(
        FakeLoggingInfoNanoDriver.class.getClassLoader(), new Class<?>[] {NanoDriver.class},
        new LoggingInvocationHandler(Level.INFO, remoteNanoDriver(host, port)));
    } else {
      nanoDriver = NanoDriverUtil.localNanoDriver(nanoDriverClassname, config);
    }
    return nanoDriver;
  }

  static NanoDriver remoteNanoDriver(String host, int port) {
    try {
      ServiceResolver serviceResolver = new ServiceResolver(new SocketTransport(
        new ConsoleClientMonitor(), null, new NeverConnectionPinger(), Thread.currentThread().getContextClassLoader(),
        new ByteStream(), new SocketDetails(host, port)));
      return serviceResolver.resolveService("it.unibo.refolding.pico.RemoteNanoDriver");
    } catch (RedirectedException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    } catch (ConnectionException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
  }

  public static final Pattern hookePathPattern = Pattern.compile("\\bpath=\"([^\"]+)\"");

  static final Pattern csvPattern = Pattern.compile("^([^,]+),\\s*.*$");

  public static Option createInputPathListOption() {
    Option inputPathListOption = new Option("inputPathList", true, "Used only to get the files.");
    inputPathListOption.setRequired(true);
    inputPathListOption.setType(String.class);
    return inputPathListOption;
  }

  public static List<Path> computeFileList(
    String dataDir, String inputPathList, String wildcardPattern) throws IOException {
    final List<Path> files;
    if (!StringUtils.isEmpty(dataDir)) {
      Path directory = java.nio.file.Paths.get(dataDir);
      files = Arrays.asList(CommonThings.listDirOrNull(directory, wildcardPattern));
    } else {
      files = new ArrayList<>();
      Path inputPathListPath = Paths.get(inputPathList);
      PathMatcher pathMatcher = inputPathListPath.getFileSystem().getPathMatcher(wildcardPattern);
      List<String> lines = Files.readAllLines(inputPathListPath, Charsets.ISO_8859_1);
      int parsingState = 0;
      for (String line : lines) {
        Matcher m = hookePathPattern.matcher(line);
        boolean found = m.find();
        if (!found) {
          m = curveStretchPattern.matcher(line);
          found = m.matches();
        }
        if (!found) {
          m = RetractionCurve.curvePattern.matcher(line);
          found = m.matches();
        }
        if (!found) {
          m = RetractionCurve.oldCurvePattern.matcher(line);
          found = m.matches();
        }
        // Hooke header and footer don't have any commas.
        if (!found) {
          m = csvPattern.matcher(line);
          // Match full line.
          found = m.matches();
        }
        if (!found) {
          if (parsingState == 1) {
            parsingState = 2;
          }
        } else {
          Preconditions.checkState(parsingState == 0 | parsingState == 1);
          String f = m.group(1);
          Path path = Paths.get(f);
          if (pathMatcher.matches(path)) {
            logger.info(path.toString());
            files.add(path);
          }
          parsingState = 1;
        }
      }
    }
    return files;
  }

  static Option createApproachSkipDistanceNmOption() {
    Option approachSkipDistanceNmOption = new Option("approachSkipDistanceNm", true, "");
    approachSkipDistanceNmOption.setRequired(true);
    return approachSkipDistanceNmOption;
  }

  public static List<PicoData> readPicoData(
    FileFormat format, Path file, HeaderParserStrategy headerParserStrategy) {
    switch (format) {
    case NANOSCOPE:
      return readNanoscope(file, headerParserStrategy);
    case JPK:
      return ImmutableList.<PicoData>copyOf(JpkDataExtractor.parse(file));
    default:
      PlainPicoData x = zeroOrOnePlainPicoDatToList(format, file);
      return x == null ? ImmutableList.<PicoData>of() : ImmutableList.<PicoData>of(x);
    }
  }

  public static PlainPicoData zeroOrOnePlainPicoDatToList(FileFormat format, Path file) {
    switch (format) {
    case MFP:
      return readMfp(file);
    case CSV:
      return CsvDataExtractor.parse(file);
    default:
      throw new IllegalStateException("should never happen");
    }
  }

  private static PlainPicoData readMfp(Path lvdtPath) {
    /*
     * It must be LVDT followed by at least 4 digits. Ideally, before that it should not be "After". Then replace LVDT with deflection.
     */
    Path parent = lvdtPath.getParent();
    String lvdtFilename = lvdtPath.getFileName().toString();
    String deflectionFilename = CommonThings.replaceLastOccurenceOrFail(lvdtFilename, "LVDT", "deflection");
    Path deflectionPath = parent.resolve(deflectionFilename);
    PlainPicoData parsed = MfpDataExtractor.parse(deflectionPath, lvdtPath);
    return parsed;
  }

  private static List<PicoData> readNanoscope(Path file, HeaderParserStrategy headerParserStrategy) {
    PicoData parsed = PicoDataExtractor.parse(file, headerParserStrategy);
    return parsed == null ? ImmutableList.<PicoData>of() : ImmutableList.of(parsed);
  }

  public static Option createWildcardPatternOption() {
    Option wildcardPatternOption = new Option("wildcardPattern", true, "wildcardPattern for files");
    wildcardPatternOption.setRequired(true);
    return wildcardPatternOption;
  }

  public static Option createDebugPatternOption() {
    return new Option("debugPattern", true, "debugPattern for files");
  }

  public static Option createDataDirOption() {
    Option dataDirOption = new Option("dataDir", true, "directory with files");
    dataDirOption.setRequired(true);
    dataDirOption.setType(String.class);
    return dataDirOption;
  }

  public static Option createFixedDeflectionLsbInNmOption() {
    Option fixedDeflectionLsbInNmOption = new Option(
      "fixedDeflectionLsbInNm", true, "fixedDeflectionLsbInNm");
    fixedDeflectionLsbInNmOption.setRequired(false);
    fixedDeflectionLsbInNmOption.setType(String.class);
    return fixedDeflectionLsbInNmOption;
  }

  public static Option createFixedDeflectionSensitivityNmPerVOption() {
    Option fixedDeflectionSensitivityNmPerVOption = new Option(
      "fixedDeflectionSensitivityNmPerV", true, "fixedDeflectionSensitivityNmPerV");
    fixedDeflectionSensitivityNmPerVOption.setRequired(false);
    fixedDeflectionSensitivityNmPerVOption.setType(String.class);
    return fixedDeflectionSensitivityNmPerVOption;
  }

  public static Option createFixedSpringConstantNPerMOption() {
    Option fixedSpringConstantNPerMOption = new Option(
      "fixedSpringConstantNPerM", true, "fixedSpringConstantNPerM");
    fixedSpringConstantNPerMOption.setRequired(false);
    fixedSpringConstantNPerMOption.setType(String.class);
    return fixedSpringConstantNPerMOption;
  }

  public static ShortList fullZ(List<PlainPicoData> picoDataList) {
    int n = 0;
    for (PlainPicoData x : picoDataList) {
      n += x.sharedApproach().timeInstantCount() + x.sharedRetraction().timeInstantCount();
    }
    short[] z = new short[n];
    int i = 0;
    for (PlainPicoData x : picoDataList) {
      System.arraycopy(x.sharedApproach().sharedZArray(), 0, z, i, x.sharedApproach().timeInstantCount());
      CommonThings.reverse(z, i, i + x.sharedApproach().timeInstantCount());
      i += x.sharedApproach().timeInstantCount();
      System.arraycopy(x.sharedRetraction().sharedZArray(), 0, z, i, x.sharedRetraction().timeInstantCount());
      i += x.sharedRetraction().timeInstantCount();
    }
    Preconditions.checkState(i == n);
    return ShortLists.unmodifiable(ShortArrayList.wrap(z));
  }

  public static ShortList fullDeflection(List<PlainPicoData> picoDataList) {
    int n = 0;
    for (PlainPicoData x : picoDataList) {
      n += x.sharedApproach().timeInstantCount() + x.sharedRetraction().timeInstantCount();
    }
    short[] def = new short[n];
    int i = 0;
    for (PlainPicoData x : picoDataList) {
      System.arraycopy(
        x.sharedApproach().sharedDeflectionArray(), 0, def, i, x.sharedApproach().timeInstantCount());
      CommonThings.reverse(def, i, i + x.sharedApproach().timeInstantCount());
      i += x.sharedApproach().timeInstantCount();
      System.arraycopy(
        x.sharedRetraction().sharedDeflectionArray(), 0, def, i, x.sharedRetraction().timeInstantCount());
      i += x.sharedRetraction().timeInstantCount();
    }
    Preconditions.checkState(i == n);
    return ShortLists.unmodifiable(ShortArrayList.wrap(def));
  }

  public static List<PlainPicoData> stripChartToPlainPicoDataList(
    StripChartPicoData stripchart, IntList jp) {
    int k = ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1;
    int iterations = jp.size() / k;
    // Compute actual velocities, ramp sizes and and waiting time.
    ApproachRetractionLoop loop = PicoDataExtractor.createLoopGivenJunctionPoints(
      stripchart, Double.NaN, Double.NaN, Double.NaN, Double.NaN, iterations, jp);
    return Collections.unmodifiableList(Arrays.asList(loop.iterations));
  }

  public static Parsed parse(
    Path file, FileFormat fileFormat, FetchRefoldBreakParams fetchRefoldBreakParams,
    LoopParams loopParams, DoubleList allowedTimeRelativeError, Path junctionPointsDir)
    throws IOException {
    final Parsed p;
    List<PlainPicoData> picoDataList;
    switch (fileFormat) {
    case NANOSCOPE:
      PicoData parsed = PicoDataExtractor.parse(file, HeaderParserStrategy.JREGEX);
      if (parsed == null) {
        logger.log(Level.WARNING, "Could not parse {0} as a {1} file.", new Object[] {
        file, fileFormat});
        p = Parsed.NOT_PARSED;
      } else if (parsed instanceof PlainPicoData) {
        PlainPicoData plain = (PlainPicoData) parsed;
        picoDataList = ImmutableList.of(plain);
        p = Parsed.from(picoDataList);
      } else {
        StripChartPicoData stripchart = (StripChartPicoData) parsed;
        Path jpPath = JpHelper.junctionPointsFile(junctionPointsDir, file);
        final IntList jp;
        if (Files.isRegularFile(jpPath)) {
          jp = JpHelper.extractJunctionPoints(jpPath);
        } else {
          if (fetchRefoldBreakParams != null) {
            jp = JpHelper.computeJunctionPoints(
              fetchRefoldBreakParams, stripchart, allowedTimeRelativeError);
          } else if (loopParams != null) {
            jp = PicoDataExtractor.computeLoopJunctionPointsOrDefault(
              stripchart, 0, stripchart.timeInstantCount(), loopParams, allowedTimeRelativeError);
          } else {
            jp = IntLists.EMPTY_LIST;
          }
          if (jpPath != null) {
            JpHelper.writeToFile(jp, jpPath);
          }
        }
        p = new Parsed(
          stripchart.sharedApproach().sharedZList(),
          stripchart.sharedApproach().sharedDeflectionList(), jp, stripChartToPlainPicoDataList(
            stripchart, jp));
      }
      break;
    case JPK:
      picoDataList = JpkDataExtractor.parse(file);
      p = Parsed.from(picoDataList);
      break;
    default:
      PlainPicoData x = zeroOrOnePlainPicoDatToList(fileFormat, file);
      if (x == null) {
        p = Parsed.NOT_PARSED;
      } else {
        picoDataList = ImmutableList.of(x);
        p = Parsed.from(picoDataList);
      }
    }
    return p;
  }

  public static PlainPicoData firstApproachLastRetraction(List<PlainPicoData> plainPicoDataList) {
    PicoUtil.checkSameSettings(plainPicoDataList);
    PlainPicoData first = plainPicoDataList.get(0);
    PlainPicoData last = CommonThings.last(plainPicoDataList);
    return first.withRetraction(last.sharedRetraction());
  }

  public static void checkSameSettings(List<PlainPicoData> plainPicoDataList) {
    PlainPicoData first = plainPicoDataList.get(0);
    for (PlainPicoData plainPicoData : plainPicoDataList) {
      Preconditions.checkArgument(first.withEmptyApproachRetractionAndNoDelay().equals(
        plainPicoData.withEmptyApproachRetractionAndNoDelay()));
    }
  }

  public static Parsed fix(
    Parsed p, double fixedDeflectionSensitivityMPerV, double fixedSpringConstantNPerM) {
    if (!Double.isNaN(fixedDeflectionSensitivityMPerV)) {
      p = p.withDeflectionSensitivity(fixedDeflectionSensitivityMPerV);
    }
    if (!Double.isNaN(fixedSpringConstantNPerM)) {
      p = p.withSpringConstant(fixedSpringConstantNPerM);
    }
    return p;
  }
}
